using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using WCell.Constants;
using WCell.Core.Initialization;
using WCell.RealmServer.Entities;
using WCell.RealmServer.Global;
using WCell.RealmServer.Groups;
using WCell.RealmServer.Handlers;
using NLog;
using WCell.Util.DynamicAccess;
using WCell.Constants.World;
using WCell.Util.Graphics;

namespace WCell.RealmServer.Instances
{
	/* TODO: Add in instance, heroic, and raid checks before entering
     *  - Quests, Items, Raid
	 */
	public class InstanceMgr
	{
		private static readonly Logger log = LogManager.GetCurrentClassLogger();

		/// <summary>
		/// The max amount of instances that a Character may enter within InstanceCooldown
		/// </summary>
		public static int MaxInstancesPerCharPerHour = 5;

		/// <summary>
		/// Amount of time until a normal empty Instance expires by default
		/// </summary>
		public static int NormalExpiryMinutes = 30;

		/// <summary>
		/// Players may only enter MaxInstancesPerCharPerHour instances within this cooldown
		/// </summary>
		public static TimeSpan InstanceCooldown = TimeSpan.FromHours(1);

		public static readonly Dictionary<uint, InstanceInfo> OfflinePlayers = new Dictionary<uint, InstanceInfo>();

		public static readonly List<RegionInfo> InstanceInfos = new List<RegionInfo>();

		private static readonly ReaderWriterLockSlim s_syncLock = new ReaderWriterLockSlim();

		#region Properties
		#endregion

		#region Enter & Creation
		public static void SetCreator(MapId id, string typeName)
		{
			var type = RealmServer.GetType(typeName);
			if (type == null)
			{
				log.Warn("Invalid Creator for Instance \"" + id + "\": " + typeName + "  - " +
											"Please correct it in the Instance-config file: " + InstanceConfig.Filename);
				return;
			}
			var producer = AccessorMgr.GetOrCreateDefaultProducer(type);
			SetCreator(id, () => (BaseInstance)producer.Produce());
		}

		public static void SetCreator(MapId id, InstanceCreator creator)
		{
			var info = World.GetRegionInfo(id);
			if (info != null && info.InstanceTemplate != null)
			{
				info.InstanceTemplate.Creator = creator;
			}
			else
			{
				throw new ArgumentException("Given Map is not an Instance:" + id);
			}
		}

		public static I CreateInstance<I>(Character creator, InstanceTemplate template, DungeonMode mode)
			where I : BaseInstance, new()
		{
			return (I)SetupInstance(creator, new I(), template, mode);
		}

		public static BaseInstance CreateInstance(Character creator, InstanceTemplate template, DungeonMode mode)
		{
			var instance = template.Create();
			return SetupInstance(creator, instance, template, mode);
		}

		static BaseInstance SetupInstance(Character creator, BaseInstance instance, InstanceTemplate template, DungeonMode mode)
		{
			if (instance != null)
			{
				instance.m_InstanceId = template.RegionInfo.NextId();
				instance.m_regionInfo = template.RegionInfo;
				instance.DungeonMode = mode;
				if (creator != null)
				{
					instance.Owner = creator.InstanceLeader;
				}

				World.AddRegion(instance);
			}

			return instance;
		}

		/// <summary>
		/// This is called when an area trigger causes entering an instance
		/// TODO: Add associations to raid individuals
		/// </summary>
		public static bool EnterInstance(Character chr, RegionInfo regionInfo, Vector3 targetPos)
		{
			chr.SendSystemMessage("Entering instance...");

			if (!regionInfo.IsInstance)
			{
				log.Error("Character {0} tried to enter \"{1}\" as Instance.",
				          chr, regionInfo);
				return false;
			}

			var isRaid = (regionInfo.Type == MapType.Raid);
			var group = chr.Group;
			if (isRaid && !chr.Role.IsStaff && !(group is RaidGroup))
			{
				InstanceHandler.SendRequiresRaid(chr.Client);
				return false;
			}

			if (!regionInfo.MayEnter(chr))
			{
				return false;
			}

			// Find out if we've been here before.
			var instances = chr.Instances;
			var firstEnter = true;
			BaseInstance instance = null;
			if (instances != null)
			{
				instance = instances.GetActiveInstance(regionInfo);
				firstEnter = (instance == null);
			}

			if (firstEnter)
			{
				if (!isRaid && !chr.Instances.HasFreeInstanceSlot)
				{
					InstanceHandler.SendTransferFailure(chr.Client, regionInfo.Id, InstanceTransferFailed.TooManyInstancesRecently);
					return false;
				}

				if (group != null)
				{
					instance = group.GetActiveInstance(regionInfo);
					if (instance != null)
					{
						if (instance.MaxPlayerCount != 0 && instance.PlayerCount >= instance.MaxPlayerCount)
						{
							chr.SendSystemMessage("Instance full: {0}/{1}", instance.PlayerCount, instance.MaxPlayerCount);
							InstanceHandler.SendTransferFailure(chr.Client, instance.Id, InstanceTransferFailed.InstanceFull);
							return false;
						}
					}
				}

				if (instance == null)
				{
					// create new instance
					instance = CreateInstance(chr, regionInfo.InstanceTemplate, GetGroupDungeonMode(group));
					if (instance == null)
					{
						log.Warn("Could not create Instance: " + regionInfo);
						return false;
					}
				}
			}
			else
			{
				// Check that the Raid members and Raid Leader are trying to run the same saved Raid ...
				if (isRaid && group != null)
				{
					var leaderRaid = group.Leader.Character.Instances.GetTimedInstance(regionInfo.Id);
					var playerRaid = chr.Instances.GetTimedInstance(regionInfo.Id);

					if (playerRaid == null)
					{
						// Player has never been here, use the Raid Leader's saved instance info.
						chr.Instances.SaveTimedInstance(leaderRaid);
					}
					else
					{
						if (leaderRaid != playerRaid)
						{
							// Player has this Raid saved as a different version than the Raid Leader. Sucks to be you.
							InstanceHandler.SendTransferFailure(chr.Client, instance.Id, InstanceTransferFailed.InstanceNotFound);
							return false;
						}
					}
				}
			}

			instance.TeleportInside(chr, targetPos);
			return true;
		}

		/// <summary>
		/// This is called when an area trigger causes leaving an instance
		/// TODO: Add associations to raid individuals
		/// TODO: Implement the 5 instances per hour limit, simple but needs the right spot
		/// </summary>
		public static void LeaveInstance(Character player, RegionInfo regionInfo, Vector3 entryInfo)
		{
			var region = World.GetRegion(regionInfo.Id);
			player.TeleportTo(region, entryInfo);
		}
		#endregion

		#region InstanceLogs
		/// <summary>
		/// 
		/// </summary>
		/// <param name="lowId"></param>
		/// <param name="autoCreate"></param>
		/// <returns></returns>
		public static InstanceInfo GetOfflineLog(uint lowId, bool autoCreate)
		{
			return GetOfflineLog(lowId, autoCreate, false);
		}

		public static void RemoveLog(uint lowId)
		{
			var log = GetOfflineLog(lowId, false, true);
			if (log != null)
			{
				// TODO: Remove from active instances
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="lowId"></param>
		/// <param name="autoCreate"></param>
		/// <param name="remove"></param>
		/// <returns></returns>
		public static InstanceInfo GetOfflineLog(uint lowId, bool autoCreate, bool remove)
		{
			InstanceInfo instanceInfo = null;

			if (OfflinePlayers.ContainsKey(lowId))
			{
				instanceInfo = OfflinePlayers[lowId];
				if (remove)
					OfflinePlayers.Remove(lowId);

			}

			if (autoCreate)
			{
				instanceInfo = new InstanceInfo(lowId);
				if (!remove)
					OfflinePlayers.Add(lowId, instanceInfo);
			}

			return instanceInfo;
		}

		public static DungeonMode GetGroupDungeonMode(Group group)
		{
			return group == null ? DungeonMode.Normal : group.DungeonMode;
		}


		/// <summary>
		/// Gets and removes the InstanceLog for the given Character
		/// </summary>
		/// <param name="character"></param>
		internal static void RetrieveLog(Character character)
		{
			s_syncLock.EnterReadLock();
			try
			{
				var log = GetOfflineLog(character.EntityId.Low, false, true);
				if (log != null)
				{
					character.Instances = log;
					log.ReconnectCharacter(character);
				}
			}
			finally
			{
				s_syncLock.ExitReadLock();
			}
		}

		//We have to remove all refences to this character
		internal static void OnCharacterLogout(Character character)
		{
			if (!character.HasInstanceCollection)
			{
				return;
			}
			s_syncLock.EnterWriteLock();

			try
			{
				character.Instances.Character = null;

				OfflinePlayers[character.EntityId.Low] = character.Instances;
			}
			finally
			{
				s_syncLock.ExitWriteLock();
			}
		}
		#endregion

		#region Init
		[Initialization(InitializationPass.Fifth, "Initialize Instances")]
		public static void Initialize()
		{
			InstanceInfos.Sort();
			InstanceConfig.LoadSettings();
		}
		#endregion

		//public static IInstanceHolderSet
	}
}
